package util;

import com.fasterxml.jackson.core.JsonProcessingException;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import io.netty.bootstrap.Bootstrap;
import io.netty.bootstrap.ServerBootstrap;
import io.netty.buffer.ByteBuf;
import io.netty.buffer.Unpooled;
import io.netty.channel.*;
import io.netty.channel.nio.NioEventLoopGroup;
import io.netty.channel.socket.SocketChannel;
import io.netty.channel.socket.nio.NioServerSocketChannel;
import io.netty.channel.socket.nio.NioSocketChannel;
import io.netty.handler.codec.LengthFieldBasedFrameDecoder;
import io.netty.util.CharsetUtil;
import it.unisa.dia.gas.jpbc.Element;
import it.unisa.dia.gas.jpbc.Field;
import it.unisa.dia.gas.jpbc.Pairing;
import it.unisa.dia.gas.plaf.jpbc.pairing.PairingFactory;
import tpase.StorageServer.VO.Address;
import tpase.StorageServer.VO.MessageCode;
import tpase.StorageServer.VO.VerifyInfo;

import javax.crypto.Cipher;
import javax.crypto.SecretKey;
import javax.crypto.spec.SecretKeySpec;
import java.io.*;
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.List;
import java.util.Map;

public class Util {
    public static void main(String[] args) throws Exception {

    }

    public static <T> T deserialize(byte[] byteArray, Class<T> cls) {
        ByteArrayInputStream bis = new ByteArrayInputStream(byteArray);
        ObjectInputStream ois = null;
        T object = null;
        try {
            ois = new ObjectInputStream(bis);
            object = (T) ois.readObject();
            ois.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
        return object;
    }

    // serialize the object into an array of bytes
    public static byte[] serialize(Object object) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(bos);
            oos.writeObject(object);
            oos.flush();
            oos.close();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return bos.toByteArray();
    }

    public static byte[] stringToBytes(String s) {
        ByteArrayOutputStream bos = new ByteArrayOutputStream();
        ObjectOutputStream oos = null;
        try {
            oos = new ObjectOutputStream(bos);
            oos.write(s.getBytes());
            return bos.toByteArray();
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
    }

    public static ByteBuf stringToByteBuf(String s) {
        return Unpooled.copiedBuffer(s, CharsetUtil.UTF_8);
    }

    public static String byteBufToString(ByteBuf byteBuf) {
        return byteBuf.toString(CharsetUtil.UTF_8);
    }

    public static void writeJson(String path, Object obj) {
        try {
            String s = toJson(obj);
            BufferedWriter writer = new BufferedWriter(new FileWriter(path));
            writer.write(s);
            writer.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static String toJson(Object obj) {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        try {
            return mapper.writeValueAsString(obj);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T jsonToObject(String json, Class<T> cls) {
        ObjectMapper mapper = new ObjectMapper();
        try {
            return mapper.readValue(json, cls);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }


    public static Pairing getPairing(String path) {
        return PairingFactory.getPairing(path);
    }

    public static <T> T jsonToObject(String json, Class<?> parameterized, Class<?>... parameterClasses) {
        ObjectMapper mapper = new ObjectMapper();
        JavaType javaType = mapper.getTypeFactory().constructParametricType(parameterized, parameterClasses);
        try {
            return mapper.readValue(json, javaType);
        } catch (JsonProcessingException e) {
            throw new RuntimeException(e);
        }
    }

    public static <T> T jsonFileToObj(String path, Class<T> cls) {
        ObjectMapper mapper = new ObjectMapper();
        T t = null;
        try {
            File f = new File(path);
            FileReader fileReader = new FileReader(f);
            Reader reader = new InputStreamReader(new FileInputStream(f), "Utf-8");
            int ch = 0;
            StringBuffer sb = new StringBuffer();
            while ((ch = reader.read()) != -1) {
                sb.append((char) ch);
            }
            fileReader.close();
            reader.close();
            String jsonStr = sb.toString();
            t= mapper.readValue(jsonStr, cls);
            //t = mapper.readValue(new File(path), cls);
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return t;
    }

    public static byte[] hashToBytes(String msg) {
        try {
            MessageDigest digest = MessageDigest.getInstance("SHA-256");
            return digest.digest(msg.getBytes(StandardCharsets.UTF_8));
        } catch (NoSuchAlgorithmException e) {
            throw new RuntimeException(e);
        }
    }

    public static Element hash(Field field, String msg) {
        byte[] hashedData = hashToBytes(msg);
        return field.newElementFromHash(hashedData, 0, hashedData.length);
    }

    public static byte[] intToByteArr(int value) {
        byte[] idxBytes = new byte[1];
        idxBytes[0] = (byte) value;
        return idxBytes;
    }



    public static void processClientBootstrap(ChannelInboundHandlerAdapter channelInboundHandlerAdapter, Address localAddress, Address connectAddress) {
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = getBootstrap(channelInboundHandlerAdapter);
            bootstrap.group(eventExecutors);

            // 客户端同一个端口不能同时和多个服务器建立连接
            bootstrap.localAddress(localAddress.getIp(), localAddress.getPort());
            ChannelFuture channelFuture = bootstrap.connect(connectAddress.getIp(), connectAddress.getPort()).sync();
            // 对通道关闭进行监听
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭线程组
            eventExecutors.shutdownGracefully();
        }
    }

    // no local port
    public static void processClientBootstrap(ChannelInboundHandlerAdapter channelInboundHandlerAdapter, Address connectAddress) {
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        try {
            Bootstrap bootstrap = getBootstrap(channelInboundHandlerAdapter);
            bootstrap.group(eventExecutors);

            // 客户端同一个端口不能同时和多个服务器建立连接
            ChannelFuture channelFuture = bootstrap.connect(connectAddress.getIp(), connectAddress.getPort()).sync();
            // 对通道关闭进行监听
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭线程组
            eventExecutors.shutdownGracefully();
        }
    }


    private static Bootstrap getBootstrap(ChannelInboundHandlerAdapter channelInboundHandlerAdapter) {
        Bootstrap bootstrap = new Bootstrap();
        bootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
        // 设置客户端的通道实现类型
        bootstrap.channel(NioSocketChannel.class);
        // 使用匿名内部类初始化通道
        bootstrap.handler(new ChannelInitializer<SocketChannel>() {
            @Override
            protected void initChannel(SocketChannel socketChannel) throws Exception {
                ChannelPipeline channelPipeline = socketChannel.pipeline();

//                    channelPipeline.addLast(new LoggingHandler(LogLevel.INFO));
                channelPipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024 * 10, 0, 4, 0, 0));
                channelPipeline.addLast(new MessageCode());
                //添加客户端通道的处理器
                channelPipeline.addLast(channelInboundHandlerAdapter);
            }
        });
        return bootstrap;
    }

    public static void processServerBootstrap(ChannelInboundHandlerAdapter channelInboundHandlerAdapter, int port) {
        EventLoopGroup bossGroup = new NioEventLoopGroup();
        EventLoopGroup workerGroup = new NioEventLoopGroup();
        try {
            ServerBootstrap serverBootstrap = new ServerBootstrap();
            serverBootstrap.group(bossGroup, workerGroup);
            serverBootstrap.channel(NioServerSocketChannel.class);
            serverBootstrap.option(ChannelOption.SO_BACKLOG, 128);
            serverBootstrap.childOption(ChannelOption.SO_KEEPALIVE, true);

            serverBootstrap.childHandler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    ChannelPipeline channelPipeline = socketChannel.pipeline();
//                    channelPipeline.addLast(new LoggingHandler(LogLevel.INFO));
                    channelPipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024 * 10, 0, 4, 0, 0));

                    channelPipeline.addLast(new MessageCode());
                    channelPipeline.addLast(channelInboundHandlerAdapter);
                }
            });
            ChannelFuture channelFuture = serverBootstrap.bind(port).sync();
            channelFuture.channel().closeFuture().sync();
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            workerGroup.shutdownGracefully();
            bossGroup.shutdownGracefully();
        }
    }

    // pass test
    public static Element getLambda_0iT(Element x, List<byte[]> idList, Pairing pairing) {
        Element lambda_0iT = pairing.getZr().newOneElement();
        for (int i = 0; i < idList.size(); i++) {
            Element e_i = pairing.getZr().newElementFromBytes(idList.get(i));
            if (e_i.equals(x)) {
                continue;
            }

            // (0 - e_i) / (x - e_i)
            Element zero = pairing.getZr().newZeroElement();
            Element up = zero.duplicate().sub(e_i);
            Element down = x.duplicate().sub(e_i);
            lambda_0iT.mulZn(up.duplicate().div(down.duplicate()));
        }
        return lambda_0iT;
    }

    public static Element getLambda_0iT_ByElement(Element x, List<Element> idList, Pairing pairing) {

        Element lambda_0iT = pairing.getZr().newOneElement();
        for (int i = 0; i < idList.size(); i++) {
            Element e_i = idList.get(i);
            if (e_i.equals(x)) {
                continue;
            }
            // (0 - e_i) / (x - e_i)
            Element zero = pairing.getZr().newZeroElement();
            Element up = zero.duplicate().sub(e_i);
            Element down = x.duplicate().sub(e_i);
            lambda_0iT.mulZn(up.duplicate().div(down.duplicate()));
        }

        return lambda_0iT;
    }

    public static void processClientBootstrap(Bootstrap bootstrap, ChannelInboundHandlerAdapter channelInboundHandlerAdapter, Address localAddress, List<Address> addressList) {
        NioEventLoopGroup eventExecutors = new NioEventLoopGroup();
        try {
            bootstrap.option(ChannelOption.SO_RCVBUF, 1024 * 1024);
            // 设置线程组
            bootstrap.group(eventExecutors);
            // 设置客户端的通道实现类型
            bootstrap.channel(NioSocketChannel.class);
            // 使用匿名内部类初始化通道
            bootstrap.handler(new ChannelInitializer<SocketChannel>() {
                @Override
                protected void initChannel(SocketChannel socketChannel) throws Exception {
                    ChannelPipeline channelPipeline = socketChannel.pipeline();

//                    channelPipeline.addLast(new LoggingHandler(LogLevel.INFO));
                    channelPipeline.addLast(new LengthFieldBasedFrameDecoder(1024 * 1024 * 10, 0, 4, 0, 0));
                    channelPipeline.addLast(new MessageCode());
                    //添加客户端通道的处理器
                    channelPipeline.addLast(channelInboundHandlerAdapter);
                }
            });

            bootstrap.localAddress(localAddress.getIp(), localAddress.getPort());
            List<ChannelFuture> channelFutureList = new ArrayList<>();
            for (Address connectAddress : addressList) {

                bootstrap.remoteAddress(connectAddress.getIp(), connectAddress.getPort());
                ChannelFuture channelFuture = bootstrap.connect().addListener((ChannelFuture futureLister) -> {
                    if (!futureLister.isSuccess()) {
                        System.out.println(connectAddress.getPort() + " fail");
                    } else {
                        System.out.println(connectAddress.getPort() + " success");
                    }
                });
//                channelFuture = bootstrap.connect(connectAddress.getIp(), connectAddress.getPort());
                System.out.println(connectAddress.getPort());

                channelFutureList.add(channelFuture);
                // 对通道关闭进行监听
//                channelFuture.channel().closeFuture();
            }

            Thread.sleep(2000);
            for (ChannelFuture channelFuture : channelFutureList) {
                channelFuture.channel().closeFuture().sync();
            }

        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            // 关闭线程组
            eventExecutors.shutdownGracefully();
        }
    }

    public static byte[] encrypt(String key, byte[] input) {
        final String SECRET = "AES";
        final String CIPHER_ALGORITHM = "AES/ECB/PKCS5Padding";
        try {
            Cipher cipher = Cipher.getInstance(CIPHER_ALGORITHM);
            byte[] keyBytes = Arrays.copyOf(hashToBytes(key), 16);
            SecretKey keySpec = new SecretKeySpec(keyBytes, SECRET);
            cipher.init(Cipher.ENCRYPT_MODE, keySpec);
            return cipher.doFinal(input);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static byte[] decrypt(String key, byte[] input) {
        Cipher cipher = null;
        try {
            cipher = Cipher.getInstance("AES/ECB/PKCS5Padding");
            byte[] keyBytes = Arrays.copyOf(hashToBytes(key), 16);
            SecretKey keySpec = new SecretKeySpec(keyBytes, "AES");
            cipher.init(Cipher.DECRYPT_MODE, keySpec);
            return cipher.doFinal(input);
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }

    public static long avg(List<Long> times) {
        long sum = 0;
        for (Long time : times) {
            sum += time;
        }
        return sum / times.size();
    }

    public static Element getY1i(Element lambda_0iT, String uid, Element x_pwd, Pairing pairing, Element s_i) {
        Element H_2_uid = Util.hash(pairing.getG2(), uid);
        // e(x_pwd, H_2(uid))^{lambda_0iT * s_i}
        return pairing.pairing(x_pwd, H_2_uid).powZn(lambda_0iT.duplicate().mulZn(s_i));
    }

    public static Element getY1iWithoutLambda(String uid, Element x_pwd, Pairing pairing, Element s_i) {
        Element H_2_uid = Util.hash(pairing.getG2(), uid);
        // e(x_pwd, H_2(uid))^{lambda_0iT * s_i}
        return pairing.pairing(x_pwd, H_2_uid).powZn(s_i);
    }

    public static Element getY2i(Element lambda0iT, String uid, Element com_m, Pairing pairing, Element s_i) {
        Element H_4_uid_com_m = Util.hash(pairing.getG2(), uid + com_m.toString());
        H_4_uid_com_m.powZn(lambda0iT.duplicate().mulZn(s_i));
        return H_4_uid_com_m;
    }

    public static boolean verifyY1i(List<VerifyInfo> verifyList, Element eXpwdH2uid, Element gT, Map<Element, Element> xToGammaSti, Pairing pairing) {
        for (VerifyInfo verifyInfo : verifyList) {
            Element x = verifyInfo.getX();
            verifyInfo.setW(eXpwdH2uid);
            verifyInfo.setG(gT);
            verifyInfo.setGamma(xToGammaSti.get(x));
            verifyInfo.setPairing(pairing);
            if (!verifyInfo.verify()) {
                return false;
            }
        }
        return true;
    }

    private Util() {
    }

    public static Element GTToG2(Element gtElement, Pairing pairing) {
        Element g2Element = pairing.getG2().newElement();
        byte[] gtElementBytes = gtElement.toBytes();
        g2Element.setFromHash(gtElementBytes, 0, gtElementBytes.length);
        return g2Element;
    }

    public static Element getY1(List<Element> y1iList, Element rPwd, List<Element> serverXList, Pairing pairing) {
        Element Y1 = pairing.getGT().newOneElement();
        List<Element> lambdaList = new ArrayList<>();

        for (int i = 0; i < serverXList.size(); i++) {
            lambdaList.add(Util.getLambda_0iT_ByElement(serverXList.get(i), serverXList, pairing));
        }

        for (int i = 0; i < y1iList.size(); i++) {
            Element lambda = lambdaList.get(i);
            Element tmp = y1iList.get(i).duplicate().powZn(lambda);

            Y1.add(tmp);
        }
        Y1.powZn(rPwd.duplicate().invert());
        return Y1;
    }

    public static Element getY2(List<Element> y2iList, List<Element> serverXList, Pairing pairing) {
        List<Element> lambdaList = new ArrayList<>();
        for (int i = 0; i < serverXList.size(); i++) {
            lambdaList.add(Util.getLambda_0iT_ByElement(serverXList.get(i), serverXList, pairing));
        }

        Element Y2 = pairing.getG2().newOneElement();
        for (int i = 0; i < y2iList.size(); i++) {
            Y2.add(y2iList.get(i).duplicate().powZn(lambdaList.get(i)));
        }

        return Y2;
    }

    public static Element getY2iWithoutLambda(String uid, Element comM, Pairing pairing, Element s_i) {
        Element H_4_uid_com_m = Util.hash(pairing.getG2(), uid + comM.toString());
        H_4_uid_com_m.powZn(s_i);
        return H_4_uid_com_m;
    }

    public static String getString(int len) {
        StringBuilder ans = new StringBuilder();
        for (int i = 0; i < len; i++) {
            ans.append((char)('a' + (i % 26)));
        }
        return ans.toString();
    }

    public static void writeBytes(byte[] bytes, String filePath) {
        FileOutputStream fileOutputStream = null;
        try {
            fileOutputStream = new FileOutputStream(filePath);
            fileOutputStream.write(bytes);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fileOutputStream != null) {
                try {
                    fileOutputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
    }

    public static byte[] readFileToBytes(String filePath) {
        byte[] data;
        FileInputStream fileInputStream = null;
        try {
            fileInputStream = new FileInputStream(filePath);
            data = new byte[fileInputStream.available()];
            fileInputStream.read(data);
        } catch (IOException e) {
            throw new RuntimeException(e);
        } finally {
            if (fileInputStream != null) {
                try {
                    fileInputStream.close();
                } catch (IOException e) {
                    throw new RuntimeException(e);
                }
            }
        }
        return data;
    }

    public static String getFileName(String folderPathString, String fileName) {
        String resultFileName = fileName;
        File file = new File(folderPathString + resultFileName);

        int lastDotIndex = fileName.lastIndexOf('.');
        if (lastDotIndex == -1) {
            lastDotIndex = fileName.length();
        }

        int i = 1;
        while (file.exists()) {
            String tmpFileName = new StringBuilder(fileName).insert(lastDotIndex, " (" + i + ")").toString();
            resultFileName = tmpFileName;
            file = new File(folderPathString + tmpFileName);
            i++;
        }
        return resultFileName;
    }
}
